﻿using System;
using System.Timers;

namespace Samurai.Wakizashi.Caching
{
    /// <summary>
    /// Interface to support the expiration of cached items and a delegate 
    /// action to execute upon expiration.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public interface IExpiringCacheStore
    {
        /// <summary>
        /// Gets or sets the expiration time span.
        /// </summary>
        /// <value>The expiration time span.</value>
        TimeSpan ExpirationTimeSpan { get; set; }

        /// <summary>
        /// Gets or sets the action to execute upon cache expiration.
        /// </summary>
        /// <value>The action to execute.</value>
        Action<IExpiringCacheStore> ExpireAction { get; set; }
    }

    /// <summary>
    /// Cache store that holds like-type items for a specified period of time. Once that 
    /// period of time has elapsed, items are evicted from the cache and if present, the 
    /// expiration action at the ExpirationAction property is executed.
    /// </summary>
    /// <typeparam name="T">The type of items to store in this cache.</typeparam>
    /// <remarks>
    /// <para>If no expiration period is specified, the cache will default to one minute.</para>
    /// <para>Access to the cache is syncronized and is therefore thread safe.</para>
    /// </remarks>
    public class ExpiringCacheStore<T> : CacheStore<T>, IExpiringCacheStore where T : class
    {
        private Timer _expirationTimer;

        /// <summary>
        /// Caches the provided item. If this is the first item in the cache, the 
        /// expiration timer is created and started using the <see cref="TimeSpan"/> specified 
        /// in the ExpirationTimeSpan property.
        /// </summary>
        /// <param name="item">The item to cache.</param>
        /// <exception cref="ArgumentNullException">Throws if <paramref name="item"/> parameter is null.</exception>
        public override void CacheItem(T item)
        {
            var cacheIsEmpty = Items.Count == 0;

            base.CacheItem(item);

            if (cacheIsEmpty)
                BeginExpirationClock();
        }

        #region Implementation of IExpiringCacheStore<T>

        /// <summary>
        /// Gets or sets the expiration time span.
        /// </summary>
        /// <value>The expiration time span.</value>
        public TimeSpan ExpirationTimeSpan { get; set; }

        /// <summary>
        /// Gets or sets the expire action.
        /// </summary>
        /// <value>The expire action.</value>
        public Action<IExpiringCacheStore> ExpireAction { get; set; }

        #endregion

        protected void ExpirationTimer_Elapsed(object sender, ElapsedEventArgs e)
        {
            _expirationTimer.Stop();
            _expirationTimer.Elapsed -= ExpirationTimer_Elapsed;
            _expirationTimer.Dispose();

            lock (this)
            {
                var items = AllItems;
                foreach (var item in items)
                {
                    // ReSharper disable RedundantAssignment
                    var i = item;
                    // ReSharper restore RedundantAssignment

                    Items.TryTake(out i);
                }
            }

            if (ExpireAction != null)
                ExpireAction.Invoke(this);
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification="Timer will be disposed when either restarting or when this instance is disposed. No reason to dispose within this method.")]
        private void BeginExpirationClock()
        {
            if (ExpirationTimeSpan == TimeSpan.MinValue)
                ExpirationTimeSpan = TimeSpan.FromMinutes(1);

            _expirationTimer = new Timer
            {
                Interval = ExpirationTimeSpan.Milliseconds
            };
            _expirationTimer.Elapsed += ExpirationTimer_Elapsed;
            _expirationTimer.Start();
        }

        protected override void Dispose(bool disposing)
        {
            if(_expirationTimer != null)
                _expirationTimer.Dispose();

            base.Dispose(disposing);
        }
    }
}
